package edu.siu.math.egut.util;

import java.io.Serializable;
import java.util.Arrays;
import java.util.Map;
import java.util.WeakHashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import edu.siu.math.egut.io.Algorithms;
import edu.siu.math.egut.io.SunipotentFormatException;
import edu.siu.math.egut.main.OutputStyle;

/**
 * The Class Sunipotent:  short for "Simple Unipotent";
 * logical part of a "unipotent." Let $G$ be a split connected reductive
 * algebraic group over a field F, equipped with a choice of maximal torus (used
 * to define roots) and a choice of Borel (used to define a base of simple
 * roots). To each root a there is associated a one-dimensional unipotent
 * subgroup U_a, isomorphic to the additive group-scheme. The class Sunipotent
 * models elements of these subgroups. Specifically, we take G to be equipped,
 * in addition, with a Chevalley-Steinberg system of epinglage, i.e., a family
 * of specific isomorphisms x_a between the subgroups U_a and the additive
 * group-scheme, subject to certain compatibility conditions. Then an element of
 * U_a(A) (for A an F-algebra) is of the form x_a(r), with r in A. To model this
 * and facilitate symbolic manipulation, a Sunipotent has two attributes: a
 * root, and an "argument" which is a rational function. (At the moment, 
 * only Polynomial arguments are supported properly.)
 * 
 * A Sunipotent is completely determined by its root and argument.  Thus, 
 * one will not have two Sunipotents with the same root and argument, 
 * but rather, might have the same Sunipotent appearing in two 
 * different places.  
 * 
 * @author Joe Pleso
 * @author Joseph Hundley
 */
public final class Sunipotent implements Serializable {
    /**
     * 
     */
    private static final long serialVersionUID = 126L;
    /* 
     * The "store." A Map which lists each Sunipotent object which has been created.
     */
    private static final Map<Sunipotent, Object> sunipotents = new WeakHashMap<Sunipotent, Object>();
    private static final String ABBREVIATION = "=";
 
    /*The underlying data of a Sunipotent:
     * an int[] (the root) and a Polynomial (the argument)
     */
    private final int[] a;

    private final Polynomial r;

    
    /**
     * Accesses the Sunipotent with the specified underlying data.  
     * 
     * @param i  the root 
     * @param r the argument
     * @return the specified sunipotent
     */
    public static Sunipotent create(int[] i, Polynomial r) {
	for (Sunipotent s : sunipotents.keySet())
	    if (Arrays.equals(s.a, i) && s.r == r)
		return s;
	return new Sunipotent(i, r);
    }

    
    /**
     * Instantiates a new sunipotent.  This method is used by "create,"
     * after first checking that the desired Sunipotent is not 
     * already available in the store.  Clients should normally use 
     * "create."  Use of this method directly can lead to the 
     * same Sunipotent appearing twice in the store, which, in turn,
     * can lead to both inefficiency and subtle logic errors.
     * 
     * @param a  root of the ambient group
     * @param q  the polynomial argument
     */
    private Sunipotent(int[] i, Polynomial q) {
	a = i;
	r = q;
	sunipotents.put(this, null);
    }




/**
 * s.equals(sPrime) true if the two Sunipotents s and sPrime have the same root and the same argument and 
 * false otherwise
 * 
 * roots are compared with Arrays.equals().  Polynomials are compared with ==
 * (using our class Polynomial, there should never be two distinct objects
 * of class Polynomial representing the same polynomial, i.e., containing the 
 * same data).  
 * 
 * @param s a Sunipotent  
 * @return true if the two have the same root and argument.
 */
public boolean equals(final Sunipotent s){
    
    return (s.getArgument() == getArgument())&&Arrays.equals(s.getRoot(), getRoot());
}
    /**
     * Gets the polynomial.
     * 
     * @return the polynomial
     */
    public Polynomial getArgument() {
	return r;
    }
    
     
     
    /**
     * Creates a short string representation of a root by giving 
     * just the digits.  
     * @param a2 a root.
     * @return the entries mashed together into a string.
     */
    public static String rootAsShortString(int[] a2) {
	StringBuffer sb = new StringBuffer();
	int sign = 1;
	if( edu.siu.math.egut.util.Algorithms.isNegative(a2) ){
	    sb.append("-");
	    sign = -1;
	}
	for( int i = 0 ; i < a2.length; i++)
	{
	    sb.append(a2[i]*sign);
	}
	return sb.toString();
  }
    
     /**
     * Gets the root.
     * 
     * @return the root
     */
    public int[] getRoot() {
	int[] root = new int[a.length];
	for (int i = 0; i < a.length; i++)
	    root[i] = a[i];
	return root;
    }
    public String toString() {
	return "x"+toShortString();
	/* (It may, at some time, be desirable to have various toString methods for each object.
	 * For example, one that writes human-readable ASCII for the screen and one that writes 
	 * TeX.)  At that time one might want to play around with the "default display method" 
	 * toString.
	 * 
	 */
    }
    /**
     * A toString method for class Sunipotent designed to minimize the length of the output.
     * @return a String representation of the Sunipotent
     */
    public String toShortString(){
	
	StringBuffer sb = new StringBuffer();
	int[] a = getRoot();
	int sign = 1;
	if( edu.siu.math.egut.util.Algorithms.isNegative(a) ){
	    sb.append("-");
	    sign = -1;
	}
	for( int i = 0 ; i < a.length; i++)
	{
	    sb.append(a[i]*sign);
	}
	sb.append("("+getArgument().toString()+")");
	
	return sb.toString();
    }
    /**
     * @return true if the argument of this Sunipotent contains negative exponents.
     */
    public boolean hasNegativeExponents() {
	return r.hasNegativeExponents();
    }
    /**
     * Create a Sunipotent with the specified root and an argument consisting of a 
     * single variable to the first power.
     * @param root an int[] which should be a root
     * @param string a String
     * @return Sunipotent
     */
    public static Sunipotent create(final int[] root, final String string) {
	
	return Sunipotent.create(root,Polynomial.create(string));
    }
    
    /**
     * Image of the Sunipotent which calls it under the specified simple reflection.
     * 
     * If s is a Sunipotent object representing the simple unipotent x_\alpha(r) and 
     * i is an integer (between 1 and the rank of our group inclusive) then 
     * s.reflection(i) is the Sunipotent object representing 
     * 
     * w[i].x_\alpha(r).w[i]^{-1},
     * 
     * where w[i] is the "standard" representative for the simple reflection corresponding
     * to the simple root \alpha_i.
     * 
     * w[i] = x_{\alpha_i}(1) x_{-\alpha_i}(-1) x_{\alpha_i}(1).
     * 
     * 
     * @param i integer, which should be between 1 and the semisimple rank
     * @return a Sunipotent.
     */
    public Sunipotent reflection( final int i){
	    try {
		return Sunipotent.create(Group.getGroup(a.length).simpleReflection(i, a), 
			r.times(Group.getGroup(a.length).reflectionSign(i, a)));
	    } catch (NotARootException e) {
		// TODO Auto-generated catch block
		e.printStackTrace();
	    }
	    return null;
	    
    }
    
    /**
     * The image under the action of an element of the Weyl group which has been passed as 
     * an integer.
     * 
     * The integer passed to this method will be modified by its inner workings.
     * @param weylAsInt an integer, representing an element of the Weyl group
     * @return a Sunipotent 
     */
    public Sunipotent imageUnder(int weylAsInt){
	Sunipotent answer = this;
    
	while( weylAsInt > 0 ){
	    int i = weylAsInt%10;
	    weylAsInt = weylAsInt/10;
	    answer = answer.reflection(i);
	
	}
	return answer;
    }
    /**
     * Deserialization allows sunipotents to sneak into existence without 
     * being added to the store.  This is to fix that.
     * @param s a Sunipotent
     * @return s if it exists in the store.  Otherwise, the Sunipotent element
     * from the store with the same underlying data.
     */
    public static Sunipotent update( Sunipotent s){
	if(sunipotents.containsKey(s))
	    return s;
	else
	    return Sunipotent.create(s.getRoot(), Polynomial.update(s.r));
    }
    /**
     * A string representation of the given int[], with brackets marking the beginning and end and
     * commas separating the entries.
     * @param ans an int[] 
     * @return a String representation in the style of "[1,2,3,4,3,2,1,0]"
     */
    public static String rootAsLongString(final int[] ans) {
	StringBuffer sb = new StringBuffer("[");
	for( int i = 0 ; i < ans.length - 1; i++)
	    sb.append(ans[i]+",");
	sb.append(ans[ans.length-1]+"]");
	return sb.toString();
    
    }


    /**
     * The image under the action of an element of the Weyl group which has been passed as 
     * an String.
     * 
     * @param s a String , representing an element of the Weyl group
     * @return a Sunipotent
     */
    public Sunipotent imageUnder(String s) {
	Sunipotent answer = this;
	    
	for( int j=s.length()-1; j>-1; j-- ){
	    int i = (int) s.charAt(j)-48;// the magic number 48 has the property that 'd' -48 = d for any digit d.  (that is, the 
	    answer = answer.reflection(i);// integral value assigned to the digit d as a char is equal to d+48.)
	
	}
	return answer;
      }


    /**
     * @param string to be read
     * @param g for which this string is supposed to represent a sunipotent
     * @return the Sunipotent object representing this sunipotent.
     * @throws NumberFormatException
     * @throws SunipotentFormatException
     * @throws PolynomialReadException
     * @throws NotARootException 
     */
    public static Sunipotent read(final String string, final Group g) throws NumberFormatException, SunipotentFormatException, PolynomialReadException, NotARootException {
	    int[] root = Algorithms.rootFromString(Algorithms.sunipotentRootString(string), g);
	    Polynomial r = Polynomial.read(Algorithms.sunipotentPolynomialString(string));
	    return Sunipotent.create(root, r);

    
    }


    /**
     * Tries to read in a given string as a Sunipotent element s
     * @param string
     * @return a Sunipotent parsed from the string or null to indicate failure
     */
    public static Sunipotent read(final String string) {
	
	// first test:  does our string have the correct basic "shape"  x_{#}(...), where ... is anything and # is a 
	// number with 2,4,6,7, or 8 digits, possibly preceded by a minus sign?
	Matcher m = edu.siu.math.egut.io.Algorithms.SUNIPOTENT_OF_UNSPECIFIED_RANK.matcher(string);
	if(m.matches()){
	    // if so, try to extract the substring denoted # above.
	    String s="";
	    try {
		s = edu.siu.math.egut.io.Algorithms.sunipotentRootString(string).trim();
	    } catch (SunipotentFormatException e) {
		// failure at any stage should result in this method returning null
		return null;
	    }
	    
	    // now we want to extract the root.  we'll be checking that it's actually a root of the group of 
	    // the correct rank.  to do that we need to know the putative rank, which is just the length of our
	    // substring, or else the length minus 1 if the substring starts with '-'.
	    int r=s.length()-(s.charAt(0)=='-'?1:0);
	    
	    // confirm that the rank thus obtained is indeed a rank
	    if( (r == 2)||(r == 4)||(r == 6)||(r == 7)||(r == 8)){
		
		// get the corresponding group
		Group g = Group.getGroup(r);
		
		// parse root substring into a root (i.e., an int[]) 
		int[] a;
		try {
		    a = edu.siu.math.egut.io.Algorithms.rootFromString(s, g);
		} catch (NumberFormatException e1) {
		    
		    return null;
		} catch (NotARootException e1) {
		    return null;
		}
		
		
		
		    try {
			
			// extract the polynomial
			Polynomial p = Polynomial.read(edu.siu.math.egut.io.Algorithms.sunipotentPolynomialString(string));
			
			// form sunipotent and return
			return Sunipotent.create(a, p);
			
		    } catch (PolynomialReadException e) {
			return null;
		    } catch (SunipotentFormatException e) {
			return null;
		    }// end try-catch
		    
		    
		    
		
		
	    }// end if the root string was length equal to the rank of an exceptional group
	    
	    
	}//end if the format matches a Sunipotent of unspecified rank
	
	
	
	
	
	return null;
    }


    public static Sunipotent readWithChecks(String string, Group g) {
	// first test:  does our string have the correct basic "shape"  x_{#}(...), where ... is anything and # is a 
	// number with 2,4,6,7, or 8 digits, possibly preceded by a minus sign?
	Matcher m = edu.siu.math.egut.io.Algorithms.suniString(g.getRank()).matcher(string);
	
	if(m.matches()){
	    // if so, try to extract the substring denoted # above.
	    String s="";
	    try {
		s = edu.siu.math.egut.io.Algorithms.sunipotentRootString(string).trim();
	    } catch (SunipotentFormatException e) {
		// failure at any stage should result in this method returning null
		return null;
	    }
	    
	    // now we want to extract the root.  we'll be checking that it's actually a root of the group of 
	    // the correct rank.  to do that we need to know the putative rank, which is just the length of our
	    // substring, or else the length minus 1 if the substring starts with '-'.
	    int r=s.length()-(s.charAt(0)=='-'?1:0);
	    
	    // confirm that the rank thus obtained is indeed the rank of g 
	    if( r==g.getRank()){
		
		
		// parse root substring into a root (i.e., an int[]) 
		int[] a;
		try {
		    a = edu.siu.math.egut.io.Algorithms.rootFromString(s, g);
		} catch (NumberFormatException e1) {
		    
		    return null;
		} catch (NotARootException e1) {
		    return null;
		}
		
		
		
		    try {
			
			// extract the polynomial
			Polynomial p = Polynomial.read(edu.siu.math.egut.io.Algorithms.sunipotentPolynomialString(string));
			
		
			
			// form sunipotent and return
			return Sunipotent.create(a, p);
			
		    } catch (PolynomialReadException e) {
			return null;
		    } catch (SunipotentFormatException e) {
			return null;
		    }// end try-catch
		    
		    
		    
		
		
	    }// end if the root string was length equal to the rank of an exceptional group
	    
	    
	}//end if the format matches a Sunipotent of unspecified rank
	
	
	
	
	
	return null;
    }


    /**
     * @param sty indicating how the output should be formatted
     * @return a string representation in the given output style
     */
    public String toString(final OutputStyle sty) {
	if( sty == OutputStyle.TEX)
	return "x_{"
	    + edu.siu.math.egut.io.Algorithms.tupleToShortString(this.getRoot())
	    +"}("+this.getArgument().toString() +")";
	return this.toString();
    }
    public String toString(OutputStyle sty, boolean abbreviated) {
	if( sty == OutputStyle.TEX){
	StringBuilder sb = new StringBuilder( "x_{");
	    String rootString = edu.siu.math.egut.io.Algorithms.tupleToShortString(this.getRoot());
	    sb.append(rootString);
	    sb.append("}(");
	String argString = this.getArgument().toString() +")";
	if( abbreviated){
	    if(rootString.startsWith("-") ){
		String posRootString = rootString.substring(1);
		sb.append(argString.replaceAll("\\-"+posRootString, ABBREVIATION)
			.replaceAll(posRootString, "-"+ABBREVIATION)
			.replaceAll("\\{"+ABBREVIATION+"\\}", ABBREVIATION));
	    }
	    else
		sb.append(argString.replaceAll("\\-"+rootString, "-"+ABBREVIATION)
			.replaceAll(rootString, ABBREVIATION)
			.replaceAll("\\{"+ABBREVIATION+"\\}", ABBREVIATION));
	    
	}else 
	    sb.append(argString);
	return sb.toString();
	}
	return this.toString();
    }


    /**
     * One often makes use of unipotents having many terms of the for x_a(u_a).  One 
     * may employ an abbreviation for such a term.  This causes the string representation 
     * of such a sunipotent to take up less space, and draw attention to any terms which 
     * are not of this form.
     * 
     * @param abbreviated true if abbreviation is to be used
     * @return a String representation of the Sunipotent which 
     * may be abbreviated. 
     */
    public String toString(final boolean abbreviated) {
	StringBuffer sb = new StringBuffer();
	int[] a = getRoot();
	int sign = 1;
	if( edu.siu.math.egut.util.Algorithms.isNegative(a) ){
	    sb.append("-");
	    sign = -1;
	}
	StringBuffer sb1 = new StringBuffer();
	for( int i = 0 ; i < a.length; i++)
	{
	    sb1.append(a[i]*sign);
	}
	String posRootString = sb1.toString();
	sb.append(posRootString);
	String argString = "("+getArgument().toString()+")";
	if( abbreviated){
	    if( sign == 1){
		sb.append(argString.replaceAll("\\-"+posRootString, "-"+ABBREVIATION).replaceAll(posRootString, ABBREVIATION));
	    }else
		sb.append(argString.replaceAll("\\-"+posRootString, ABBREVIATION).replaceAll(posRootString, "-"+ABBREVIATION));
	}
	else
	    sb.append(argString);
	return sb.toString();
   }


    /**
     * @param i the root
     * @param j the argument
     * @return the Sunipotent with root i and argument j.  (Here j is interpreted as a constant polynomial.)
     */
    public static Sunipotent create(final int[] i, final int j) {
	return create(i, Polynomial.create(j));
    }
    

}
